[id].vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. <template>
  2. <div class="admin--sales-form">
  3. <div v-if="isLoading" class="admin--loading">
  4. 데이터를 불러오는 중...
  5. </div>
  6. <form v-else @submit.prevent="handleSubmit" class="admin--form">
  7. <!-- 전시장 -->
  8. <div class="admin--form-group">
  9. <label class="admin--form-label">전시장 <span class="admin--required">*</span></label>
  10. <select v-model="formData.showroom_id" class="admin--form-select" required>
  11. <option value="">전시장을 선택하세요</option>
  12. <option v-for="showroom in showrooms" :key="showroom.id" :value="showroom.id">
  13. {{ showroom.name }}
  14. </option>
  15. </select>
  16. </div>
  17. <!-- 지점 -->
  18. <div class="admin--form-group">
  19. <label class="admin--form-label">지점 <span class="admin--required">*</span></label>
  20. <select v-model="formData.branch_id" class="admin--form-select" required>
  21. <option value="">지점을 선택하세요</option>
  22. <option v-for="branch in branches" :key="branch.id" :value="branch.id">
  23. {{ branch.name }}
  24. </option>
  25. </select>
  26. </div>
  27. <!-- 영업팀 -->
  28. <div class="admin--form-group">
  29. <label class="admin--form-label">영업팀 <span class="admin--required">*</span></label>
  30. <select v-model="formData.team_id" class="admin--form-select" required>
  31. <option value="">영업팀을 선택하세요</option>
  32. <option v-for="team in teams" :key="team.id" :value="team.id">
  33. {{ team.name }}
  34. </option>
  35. </select>
  36. </div>
  37. <!-- 이름 -->
  38. <div class="admin--form-group">
  39. <label class="admin--form-label">이름 <span class="admin--required">*</span></label>
  40. <input
  41. v-model="formData.name"
  42. type="text"
  43. class="admin--form-input"
  44. placeholder="이름을 입력하세요"
  45. required
  46. >
  47. </div>
  48. <!-- 직책 -->
  49. <div class="admin--form-group">
  50. <label class="admin--form-label">직책 <span class="admin--required">*</span></label>
  51. <select v-model="formData.position" class="admin--form-select" required>
  52. <option value="">직책을 선택하세요</option>
  53. <option value="팀장">팀장</option>
  54. <option value="마스터">마스터</option>
  55. <option value="차장">차장</option>
  56. <option value="과장">과장</option>
  57. <option value="대리">대리</option>
  58. <option value="주임">주임</option>
  59. <option value="사원">사원</option>
  60. </select>
  61. </div>
  62. <!-- 대표번호 -->
  63. <div class="admin--form-group">
  64. <label class="admin--form-label">대표번호 <span class="admin--required">*</span></label>
  65. <input
  66. v-model="formData.main_phone"
  67. type="tel"
  68. class="admin--form-input"
  69. placeholder="02-1234-5678"
  70. required
  71. >
  72. </div>
  73. <!-- 직통번호 -->
  74. <div class="admin--form-group">
  75. <label class="admin--form-label">직통번호</label>
  76. <input
  77. v-model="formData.direct_phone"
  78. type="tel"
  79. class="admin--form-input"
  80. placeholder="02-1234-5679"
  81. >
  82. </div>
  83. <!-- 핸드폰 -->
  84. <div class="admin--form-group">
  85. <label class="admin--form-label">핸드폰</label>
  86. <input
  87. v-model="formData.mobile"
  88. type="tel"
  89. class="admin--form-input"
  90. placeholder="010-1234-5678"
  91. >
  92. </div>
  93. <!-- 이메일 -->
  94. <div class="admin--form-group">
  95. <label class="admin--form-label">이메일</label>
  96. <input
  97. v-model="formData.email"
  98. type="email"
  99. class="admin--form-input"
  100. placeholder="email@example.com"
  101. >
  102. </div>
  103. <!-- 사진 -->
  104. <div class="admin--form-group">
  105. <label class="admin--form-label">사진</label>
  106. <input
  107. type="file"
  108. accept="image/*"
  109. class="admin--form-file"
  110. @change="handlePhotoUpload"
  111. >
  112. <div v-if="photoPreview || formData.photo_url" class="admin--image-preview">
  113. <img :src="photoPreview || formData.photo_url" alt="미리보기">
  114. <button type="button" class="admin--btn-remove-image" @click="removePhoto">
  115. 삭제
  116. </button>
  117. </div>
  118. </div>
  119. <!-- SACT -->
  120. <div class="admin--form-group">
  121. <label class="admin--form-label">SACT</label>
  122. <div class="admin--radio-group">
  123. <label class="admin--radio-label">
  124. <input v-model="formData.is_sact" type="radio" :value="true" name="is_sact">
  125. <span>예</span>
  126. </label>
  127. <label class="admin--radio-label">
  128. <input v-model="formData.is_sact" type="radio" :value="false" name="is_sact">
  129. <span>아니오</span>
  130. </label>
  131. </div>
  132. </div>
  133. <!-- TOP30 -->
  134. <div class="admin--form-group">
  135. <label class="admin--form-label">TOP30</label>
  136. <div class="admin--radio-group">
  137. <label class="admin--radio-label">
  138. <input v-model="formData.is_top30" type="radio" :value="true" name="is_top30">
  139. <span>예</span>
  140. </label>
  141. <label class="admin--radio-label">
  142. <input v-model="formData.is_top30" type="radio" :value="false" name="is_top30">
  143. <span>아니오</span>
  144. </label>
  145. </div>
  146. </div>
  147. <!-- 노출순서 -->
  148. <div class="admin--form-group">
  149. <label class="admin--form-label">노출순서</label>
  150. <input
  151. v-model.number="formData.display_order"
  152. type="number"
  153. class="admin--form-input"
  154. placeholder="숫자만 입력"
  155. min="0"
  156. >
  157. <p class="admin--form-help">숫자가 작을수록 먼저 노출됩니다.</p>
  158. </div>
  159. <!-- 버튼 영역 -->
  160. <div class="admin--form-actions">
  161. <button
  162. type="submit"
  163. class="admin--btn admin--btn-primary"
  164. :disabled="isSaving"
  165. >
  166. {{ isSaving ? '저장 중...' : '확인' }}
  167. </button>
  168. <button
  169. type="button"
  170. class="admin--btn admin--btn-secondary"
  171. @click="goToList"
  172. >
  173. 목록
  174. </button>
  175. </div>
  176. <!-- 성공/에러 메시지 -->
  177. <div v-if="successMessage" class="admin--alert admin--alert-success">
  178. {{ successMessage }}
  179. </div>
  180. <div v-if="errorMessage" class="admin--alert admin--alert-error">
  181. {{ errorMessage }}
  182. </div>
  183. </form>
  184. </div>
  185. </template>
  186. <script setup>
  187. import { ref, onMounted } from 'vue'
  188. import { useRoute, useRouter } from 'vue-router'
  189. definePageMeta({
  190. layout: 'admin',
  191. middleware: ['auth']
  192. })
  193. const route = useRoute()
  194. const router = useRouter()
  195. const { get, put, upload } = useApi()
  196. const isLoading = ref(true)
  197. const isSaving = ref(false)
  198. const successMessage = ref('')
  199. const errorMessage = ref('')
  200. const photoPreview = ref(null)
  201. const photoFile = ref(null)
  202. const showrooms = ref([])
  203. const branches = ref([])
  204. const teams = ref([])
  205. const formData = ref({
  206. showroom_id: '',
  207. branch_id: '',
  208. team_id: '',
  209. name: '',
  210. position: '',
  211. main_phone: '',
  212. direct_phone: '',
  213. mobile: '',
  214. email: '',
  215. photo_url: '',
  216. is_sact: false,
  217. is_top30: false,
  218. display_order: 0
  219. })
  220. // 필터 데이터 로드
  221. const loadFilters = async () => {
  222. const { data: showroomData } = await get('/staff/showrooms')
  223. if (showroomData) showrooms.value = showroomData
  224. const { data: branchData } = await get('/branch/list', { per_page: 1000 })
  225. if (branchData) branches.value = branchData.items || []
  226. const { data: teamData } = await get('/staff/teams')
  227. if (teamData) teams.value = teamData
  228. }
  229. // 데이터 로드
  230. const loadSales = async () => {
  231. isLoading.value = true
  232. const id = route.params.id
  233. const { data, error } = await get(`/staff/sales/${id}`)
  234. if (data) {
  235. formData.value = {
  236. showroom_id: data.showroom_id || '',
  237. branch_id: data.branch_id || '',
  238. team_id: data.team_id || '',
  239. name: data.name || '',
  240. position: data.position || '',
  241. main_phone: data.main_phone || '',
  242. direct_phone: data.direct_phone || '',
  243. mobile: data.mobile || '',
  244. email: data.email || '',
  245. photo_url: data.photo_url || '',
  246. is_sact: data.is_sact || false,
  247. is_top30: data.is_top30 || false,
  248. display_order: data.display_order || 0
  249. }
  250. }
  251. isLoading.value = false
  252. }
  253. // 사진 업로드
  254. const handlePhotoUpload = (event) => {
  255. const file = event.target.files[0]
  256. if (!file) return
  257. if (!file.type.startsWith('image/')) {
  258. alert('이미지 파일만 업로드 가능합니다.')
  259. return
  260. }
  261. photoFile.value = file
  262. const reader = new FileReader()
  263. reader.onload = (e) => {
  264. photoPreview.value = e.target.result
  265. }
  266. reader.readAsDataURL(file)
  267. }
  268. // 사진 삭제
  269. const removePhoto = () => {
  270. photoPreview.value = null
  271. photoFile.value = null
  272. formData.value.photo_url = ''
  273. }
  274. // 폼 제출
  275. const handleSubmit = async () => {
  276. successMessage.value = ''
  277. errorMessage.value = ''
  278. if (!formData.value.showroom_id || !formData.value.branch_id || !formData.value.team_id) {
  279. errorMessage.value = '전시장, 지점, 영업팀을 선택하세요.'
  280. return
  281. }
  282. if (!formData.value.name) {
  283. errorMessage.value = '이름을 입력하세요.'
  284. return
  285. }
  286. isSaving.value = true
  287. try {
  288. let photoUrl = formData.value.photo_url
  289. // 새 사진 업로드
  290. if (photoFile.value) {
  291. const formDataImage = new FormData()
  292. formDataImage.append('image', photoFile.value)
  293. const { data: uploadData, error: uploadError } = await upload('/upload/image', formDataImage)
  294. if (uploadError) {
  295. errorMessage.value = '사진 업로드에 실패했습니다.'
  296. isSaving.value = false
  297. return
  298. }
  299. photoUrl = uploadData.url
  300. }
  301. const submitData = {
  302. ...formData.value,
  303. photo_url: photoUrl
  304. }
  305. const id = route.params.id
  306. const { data, error } = await put(`/staff/sales/${id}`, submitData)
  307. if (error) {
  308. errorMessage.value = error.message || '수정에 실패했습니다.'
  309. } else {
  310. successMessage.value = '영업사원 정보가 수정되었습니다.'
  311. setTimeout(() => {
  312. router.push('/admin/staff/sales')
  313. }, 1000)
  314. }
  315. } catch (error) {
  316. errorMessage.value = '서버 오류가 발생했습니다.'
  317. console.error('Save error:', error)
  318. } finally {
  319. isSaving.value = false
  320. }
  321. }
  322. const goToList = () => {
  323. router.push('/admin/staff/sales')
  324. }
  325. onMounted(async () => {
  326. await loadFilters()
  327. await loadSales()
  328. })
  329. </script>